package udpserver;

import YottaTron.Sprites.PlayerSprite;
import YottaTron.Runner;
import YottaTron.TickEvent;
import YottaTron.YotTimer;
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import org.lwjgl.util.Timer;

public class Server extends Thread implements TickEvent {

    static final byte SIGN = 0;
    static final byte NICK = 1;
    static final byte UNSIGN = 2;
    static final byte MSG = 3;
    static final byte ID = 4;
    static final byte CLIENTS = 5;
    static final byte MULTICAST = 6;
    static final byte NEW_PLAYER = 7;
    //static final byte PLAYERS = 8;
    static final byte KEY_UP = 8;
    static final byte KEY_DOWN = 9;
    static final byte KEY_LEFT = 10;
    static final byte KEY_RIGHT = 11;
    static final byte MOUSE_LEFT_PRESSED = 12;
    static final byte MOUSE_AIM = 13;
    static final byte MAP = 14;
    static final byte MAP_OVER = 15;
    static final byte HUGE = 16;
    static final byte HUGE_NEXT = 17;
    static final byte HUGE_START = 18;
    static final byte SYNCHRONIZE = 19;
    static final byte CLIENT_INPUT = 20;
    static final byte BIT = 21;
    static final byte DEAD = 22;
    static final byte SPEED = 23;
    static final byte FINISHED_ACTION = 24;
    static final byte TIME_STAMP = 25;
    static final byte POSITION = 26;
    static final int PACKET_SIZE = 512;
    static final int PORT = 1234;  // port for this server
    public DatagramChannel udpChannel;
    private ByteBuffer outBuffer;
    private ByteBuffer inBuffer;
    private ByteBuffer synchronizeBuffer;
    ArrayList<Client> clients;
    int lastID = 1000;
    Runner runner;
    int hugeDataSent = 0;
    byte[] hugeData;
    public int trafficOutTotal = 0;
    public int trafficInTotal = 0;
    ServerMonitorFrame serverMonitor;
    YottaTron.YotTimer synchronizationTimer;
    public int numberOfPackets = 0;
    public boolean synchronize = false;

    public Server() {
        runner = new Runner(false); // false znaci runner na strane serveru - bez grafiky atd.JFrame frame = new JFrame();
        // docasne pro debugovani ----------------------------
        JFrame frame = new JFrame("server");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(runner);
        frame.pack();
        frame.setVisible(true);
//-----------------------------------------------------------
        new Thread(runner).start();
        initChannels();
        clients = new ArrayList<Client>();

        // graficky vystup -----------------------------------------------------
        serverMonitor = new ServerMonitorFrame("YottaTron server", this);
        serverMonitor.setStatus();

        this.start();
        synchronizationTimer = new YotTimer(this, 33); //synchronizace bude probihat jednou za sekundu
    }

    public void initChannels() {
        try {
            udpChannel = DatagramChannel.open();
            udpChannel.socket().bind(new InetSocketAddress(PORT));
            udpChannel.configureBlocking(true);//metoda recieve zablokuje vlakno ve kterem je spustena
            outBuffer = ByteBuffer.allocateDirect(PACKET_SIZE);
            inBuffer = ByteBuffer.allocateDirect(PACKET_SIZE);
            synchronizeBuffer = ByteBuffer.allocateDirect(PACKET_SIZE);
        } catch (IOException ex) {
            System.out.println("chyba pri vytvareni udp datagram channelu");
        }
    }

    public Client getClientByAddress(SocketAddress clientAddress) {
        for (Iterator<Client> iter = clients.iterator(); iter.hasNext();) {
            Client client = iter.next();
            if (client.clientAddress.equals(clientAddress)) {
                return client;
            }
        }
        return null;
    }

    public Client getClientByID(int id) {
        for (Iterator<Client> iter = clients.iterator(); iter.hasNext();) {
            Client client = iter.next();
            if (client.id == id) {
//                System.out.println("getClientByID return id " + id);
                return client;
            }
        }
        return null;
    }

    public void sendUdpPacket(ByteBuffer b, SocketAddress clientAddress) {
        trafficOutTotal += b.limit();

        try {
            //System.out.println("outBuffer.toString() " + b.toString());
            udpChannel.send(b, clientAddress);
        } catch (IOException e) {
            System.out.println("chyba pri posilani udp packetu");
            e.printStackTrace(System.err);
        }
    }

    public void sendUdpPacketToAll(ByteBuffer b) {
        for (Iterator<Client> iter = clients.iterator(); iter.hasNext();) {
            Client client = iter.next();
            sendUdpPacket(b, client.clientAddress);
            b.rewind();
        }
    }

    public void sendID(SocketAddress clientAddress, int id) {
        outBuffer.clear();
        outBuffer.put(ID);
        outBuffer.putInt(id);
        outBuffer.flip();

        System.out.println("sending id " + id);
        sendUdpPacket(outBuffer, clientAddress);
    }

    public void broadcastMessage(int id, String msg) {
        outBuffer.clear();
        outBuffer.put(MSG);
        outBuffer.putInt(id);
        outBuffer.put((getClientByID(id).nick + ": ").getBytes());//pred zpravu pridame nick
        outBuffer.put(msg.getBytes());
        outBuffer.flip();

        System.out.println("broadcast message from id " + id);
        System.out.println("buffer: " + outBuffer.toString());
        for (Iterator<Client> iter = clients.iterator(); iter.hasNext();) {
            Client client = iter.next();
            System.out.println("send msg to " + client.id);
            sendUdpPacket(outBuffer, client.clientAddress);
            outBuffer.rewind();
        }
    }

    /* odesle konkretnimu klientovi postupne seznam vsech hracu pripojenych k serveru
     */
    public void sendPlayers(int id) {
        Client client = getClientByID(id);

        for (Iterator<PlayerSprite> iter = runner.getPlayers().iterator(); iter.hasNext();) {
            PlayerSprite player = iter.next();
            if (player.getID() != id) {
                outBuffer.clear();
                outBuffer.put(NEW_PLAYER);
                outBuffer.putInt(player.getID());
                outBuffer.putDouble(player.getX());
                outBuffer.putDouble(player.getY());
                outBuffer.putDouble(player.getDirection());
                outBuffer.put(player.weaponsToArray());
                outBuffer.flip();
                System.out.println("sending player " + player.getID() + " to player " + client.id);
                sendUdpPacket(outBuffer, client.clientAddress);
            }
        }
    }

    /* posle vsem klientum nove pripojeneho hrace
     */
    public void sendNewPlayerToOthers(int id, double x, double y, double direction, byte[] weapons) {
        outBuffer.clear();
        outBuffer.put(NEW_PLAYER);
        outBuffer.putInt(id);
        outBuffer.putDouble(x);
        outBuffer.putDouble(y);
        outBuffer.putDouble(direction);
        outBuffer.put(weapons);
        outBuffer.flip();
        for (Iterator<Client> iter = clients.iterator(); iter.hasNext();) {
            Client c = iter.next();
            if (c.id != id) {
                System.out.println("sending player " + id + " to client " + c.id);
                sendUdpPacket(outBuffer, c.clientAddress);
                outBuffer.rewind();
            }
        }
    }

    public void unsignPlayerFromOthers(int id) {
        outBuffer.clear();
        outBuffer.put(UNSIGN);
        outBuffer.putInt(id);
        outBuffer.flip();
        for (Iterator<Client> iter = clients.iterator(); iter.hasNext();) {
            Client c = iter.next();
            if (c.id != id) {
                System.out.println("unsigning player " + id + " from client " + c.id);
                sendUdpPacket(outBuffer, c.clientAddress);
                outBuffer.rewind();
            }
        }
    }

    //slouzi k posilani velkych dat se zarucenim doruceni
    public void sendHugeData(int id, byte[] data) {
        hugeData = data;
        Client client = getClientByID(id);
        outBuffer.clear();
        outBuffer.put(HUGE_START);
        outBuffer.putInt(data.length);
        outBuffer.flip();
        sendUdpPacket(outBuffer, client.clientAddress);

    }

    public void sendTimeStamp(int id) {
        outBuffer.clear();
        outBuffer.put(TIME_STAMP);
        Client client = getClientByID(id);
        outBuffer.flip();
        sendUdpPacket(outBuffer, client.clientAddress);
    }

    public void sendMapToPlayer(int id) {
        Client client = getClientByID(id);
        outBuffer.clear();
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(runner.map);
            oos.close();
            byte[] mapData = bos.toByteArray();
            System.out.println("Posilam mapu o velikosti " + mapData.length);
            sendHugeData(id, mapData);
        } catch (Exception e) {
            e.printStackTrace();
        }




    }

    private void addPosition(double x, double y, double direction) {
        outBuffer.putDouble(x);
        outBuffer.putDouble(y);
        outBuffer.putDouble(direction);
    }

    public void broadcastKeyUp(int clientID, byte up) {
        outBuffer.clear();
        outBuffer.put(KEY_UP);
        outBuffer.putInt(clientID);
        outBuffer.put(up);
//        addPosition(x, y, direction);
        outBuffer.flip();

        sendUdpPacketToAll(outBuffer);
    }

    public void broadcastKeyDown(int clientID, byte down) {
        outBuffer.clear();
        outBuffer.put(KEY_DOWN);
        outBuffer.putInt(clientID);
        outBuffer.put(down);
//        addPosition(x, y, direction);
        outBuffer.flip();

        sendUdpPacketToAll(outBuffer);
    }

    public void broadcastKeyLeft(int clientID, byte down) {
        outBuffer.clear();
        outBuffer.put(KEY_LEFT);
        outBuffer.putInt(clientID);
        outBuffer.put(down);
//        addPosition(x, y, direction);
        outBuffer.flip();

        sendUdpPacketToAll(outBuffer);
    }

    public void broadcastKeyRight(int clientID, byte down) {
        outBuffer.clear();
        outBuffer.put(KEY_RIGHT);
        outBuffer.putInt(clientID);
        outBuffer.put(down);
//        addPosition(x, y, direction);
        outBuffer.flip();

        sendUdpPacketToAll(outBuffer);
    }

    public void broadcastMouseLeftPressed(int clientID, byte down) {
        outBuffer.clear();
        outBuffer.put(MOUSE_LEFT_PRESSED);
        outBuffer.putInt(clientID);
        outBuffer.put(down);
        outBuffer.flip();

        sendUdpPacketToAll(outBuffer);
    }

    public void broadcastMouseAim(int clientID, short aim) {
        outBuffer.clear();
        outBuffer.put(MOUSE_AIM);
        outBuffer.putInt(clientID);
        outBuffer.putShort(aim);
        outBuffer.flip();

        sendUdpPacketToAll(outBuffer);
    }

    public void broadcastClientInput(int clientID) {
        PlayerSprite player = runner.getSpriteByID(clientID);
        outBuffer.clear();
        outBuffer.put(CLIENT_INPUT);
        outBuffer.putInt(clientID);
        outBuffer.putDouble(player.direction);
        outBuffer.putDouble(player.speed);
        outBuffer.putShort(player.netAimAngle);
        outBuffer.putFloat(player.currentTime);
        outBuffer.flip();
        sendUdpPacketToAll(outBuffer);
    }

    public void broadcastSynchronize(){
        long start = System.nanoTime();
        synchronizeBuffer.clear();
        synchronizeBuffer.put(SYNCHRONIZE);
        for (Iterator<Client> it = clients.iterator(); it.hasNext();) {
            Client client = it.next();
            PlayerSprite s = runner.getSpriteByID(client.id);
            if (s != null) {
                synchronizeBuffer.putInt(client.id);
                synchronizeBuffer.putFloat(s.currentTime);
                synchronizeBuffer.putDouble(s.getX());
                synchronizeBuffer.putDouble(s.getY());
                synchronizeBuffer.putDouble(s.getDirection());
                synchronizeBuffer.putDouble(s.getSpeed());
                synchronizeBuffer.putShort(s.netAimAngle);
                synchronizeBuffer.put(s.isMouseLeftPressed() ? (byte)1 : (byte)0);
                synchronizeBuffer.putInt(s.getHitPoints());
               
            }
            
        }
        synchronizeBuffer.flip();
        sendUdpPacketToAll(synchronizeBuffer);
        long end = System.nanoTime();
        //System.out.println(" synchronize took time  of " + (end - start) + " nanosecond");
    }

    public void tickEvent(){
        synchronize = true;
        //broadcastSynchronize();
       
    }

    @Override
    public void run() {
        byte[] buf;
        int id;
        byte bool;
        SocketAddress clientAddress;
        try {

            while (true) {
                inBuffer.clear();
                if(synchronize){
                broadcastSynchronize();
                synchronize = false;
                }

                clientAddress = udpChannel.receive(inBuffer); //přijmout UDP paket

                numberOfPackets++;
                inBuffer.flip();


                trafficInTotal += inBuffer.limit();

                try {
                    switch (inBuffer.get()) {//get automaticky presune aktualni pozici o 1
                        case SIGN: // pripojeni noveho klienta
                            System.out.println("SIGN " + clientAddress.toString());
                            if (getClientByAddress(clientAddress) == null) {
                                clients.add(new Client(lastID, clientAddress, "unknown"));
                                sendID(clientAddress, lastID);

                                lastID++;
                            } else {
                                System.out.println(" klient s touto adresou je jiz prihlaseny");
                            }
                            break;

                        case NICK: // pripojeni noveho klienta
                            System.out.println("NICK");
                            id = inBuffer.getInt();

                            //prevod zbytku ByteBufferu na String
                            buf = new byte[inBuffer.remaining()];
                            inBuffer.get(buf);
                            String nick = new String(buf);
                            System.out.println("nick id " + id);

                            System.out.println("string " + nick);
                            getClientByID(id).nick = nick;
                            sendMapToPlayer(id);
                            break;

                        case MSG:
                            System.out.println("MSG");
                            id = inBuffer.getInt();

                            //prevod zbytku ByteBufferu na String
                            buf = new byte[inBuffer.remaining()];
                            inBuffer.get(buf);
                            String msg = new String(buf);

                            System.out.println("client id " + id + " msg " + msg);
                            broadcastMessage(id, msg);
                            break;

                        case UNSIGN:
                            System.out.println("UNSIGN");
                            id = inBuffer.getInt();
                            clients.remove(getClientByID(id));
                            runner.removePlayer(id);
                            unsignPlayerFromOthers(id);
                            System.out.println("client with id " + id + " removed");
                            System.out.println("number of remaining clients: " + clients.size());
                            break;
                        case Server.CLIENTS:
                            System.out.println("CLIENTS");
                            //multicastClientsList(clientAddress);
                            break;
                        case Server.NEW_PLAYER:
                            int clientID = inBuffer.getInt();
                            double x = inBuffer.getDouble();
                            double y = inBuffer.getDouble();
                            double direction = inBuffer.getDouble();
                            byte[] weapons = new byte[10];
                            for (int i = 0; i < weapons.length; i++) {
                                weapons[i] = inBuffer.get();
                            }
                            //msg = new String(data, 1, packet.getLength() - 1);
                            System.out.println("NEW_PLAYER: x" + x + " y " + y + " direction " + direction + " id " + clientID);
                            if (runner.getSpriteByID(clientID) == null) {
                                runner.addPlayer(x, y, direction, clientID, weapons); //hrac se prida do seznamu hracu na strane serveru
                                System.out.println("k runneru pridan hrac s id " + clientID);
                            }
                            sendPlayers(clientID);// hracovi se poslou jiz prihlaseni hraci
                            sendNewPlayerToOthers(clientID, x, y, direction, weapons);
                            break;
                        case DEAD:
                            clientID = inBuffer.getInt();
                            System.out.println("DEAD client " + clientID);
                            runner.getSpriteByID(clientID).respawn();
                            broadcastSynchronize();
                            break;


                        case CLIENT_INPUT:
                            //System.out.println("prijimam input od klienta");
                            id = inBuffer.getInt();
                            direction = inBuffer.getDouble();
                            double speed = inBuffer.getDouble();
                            short aimAngle = inBuffer.getShort();
                            byte mouseLeftPressed = inBuffer.get();
                            float timeStamp = inBuffer.getFloat();
                            //System.out.println(direction + " " + speed);
                            
                            PlayerSprite s = runner.getSpriteByID(id);
                            if (s != null) {
                                if (s.currentTime<timeStamp) {
                                    runner.dePopulate(s);
                                    s.changeDirection(direction);
                                    s.setSpeed(speed);
                                    s.netAimAngle = aimAngle;
                                    s.setAimAngle((double) aimAngle / runner.AIM_RESOLUTION);
                                    //System.out.println("server time = " + s.currentTime + " client time = " + timeStamp);
                                    runner.handleSpritePhysics(s,  timeStamp - s.currentTime);
                                    runner.handlePlayerCooldowns(s, timeStamp - s.currentTime);
                                    runner.populate(s);
                                    if(mouseLeftPressed == 1){
                                    runner.playerShoot(s);
                                    s.setMouseLeftPressed(true);
                                    }else{
                                    s.setMouseLeftPressed(false);
                                    }
                                    //broadcastClientInput(id);
                                    s.currentTime = timeStamp;
                                    }else{
                                    System.out.println("paket je stary - zahazuji");
                                    System.out.println("server time = " + s.currentTime + " client time = " + timeStamp);
                                    }
                            }
                            break;

                             

                        case TIME_STAMP:
                            clientID = inBuffer.getInt();
                            timeStamp = inBuffer.getFloat();
                            s = runner.getSpriteByID(clientID);
                            if (s != null) {
                                s.currentTime = timeStamp;
                            }
                            sendTimeStamp(clientID);
                            break;

                        case HUGE_NEXT:
                            System.out.println("zadost od klienta na dalsi data, zacinajici bytem " + hugeDataSent);
                            if (hugeData != null) {
                                clientID = inBuffer.getInt();
                                Client client = getClientByID(clientID);
                                hugeDataSent = inBuffer.getInt();

                                outBuffer.clear();
                                outBuffer.put(HUGE);
                                outBuffer.putInt(hugeDataSent);
                                int i;
                                for (i = 0; i < outBuffer.remaining(); i++) {
                                    if (hugeData.length > (i + hugeDataSent)) {
                                        outBuffer.put(hugeData[i + hugeDataSent]);

//                                if (i == 0) {
//                                    System.out.println(hugeData[i + hugeDataSent]);
//                                }

                                    }
                                }
                                hugeDataSent += i;
                                outBuffer.flip();
                                System.out.println("odesilam paket s " + outBuffer.limit() + "bity");
                                try {
                                    sendUdpPacket(outBuffer, client.clientAddress);
                                } catch (NullPointerException e) {
                                    System.out.println("HUGE_NEXT: neexistuje client s id " + clientID);
                                }
                            }
                            break;

                        default:
                            System.out.println("neznama zprava od serveru zacinajici " + inBuffer.get(0));
                            break;
                    }
                } catch (BufferUnderflowException e) {
                    System.err.println("Prisel prazdny packet - BufferUnderflowException ve switch !!!!!!!!!!!!!");
                }

                /* vypis do stavoveho okna po kazde zmene serveru
                 */
                serverMonitor.setStatus();

            }

        } catch (IOException ex) {
            Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
        }

    }//konec run

    public static void main(String[] args) {
        Server server = new Server();
    }
}
